/*
 * RandomGenerators.java
 *
 * Created on 12 May 2006, 18:05
 *
 * Copyright (C)
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place, Suite 330, Boston, MA 02111-1307 USA
 */

package com.grubby.generators;

import com.grubby.utils.StringUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This class provides a bunch of methods to aid in random selection of things
 * such as numbers within a range and objects from an array of objects.
 * <P>
 * It was originally copied from examples provided by Jonathan Vaughan of
 * Monks/Vaughan Ltd.
 *
 * @author A. HARMEL-LAW. E: andrew.harmel.law@gmail.com
 * @author J.S. Vaughan
 * @version 0.1
 */
public final class RandomGenerators {
    
    public static final Logger LOGGER
            = Logger.getLogger("com.grubby.utils.RandomGenerators");
    
    /** Map containing counters for incremental number generation */
    private static Map<Integer, Integer> myCounters = new HashMap<Integer, Integer>();
    
    /** Constant int representing base 10 */
    private static final int BASE_10 = 10;
    
    /** Constant int representing ASCII 'A' value */
    private static final int ASCII_A = 65;
    
    /** Constant int representing ASCII 'Z' value */
    private static final int ASCII_Z = 90;
    
    /* Log Strings */
    private static final String NULL_PARAMETER_EX = "A parameter was null: ";
    
    /** Private Constructor */
    private RandomGenerators() { }
    
    /**
     * Returns a random Object from the supplied array.
     * <P>
     * The Random object will be seeded based on the current system time.
     * <P>
     * Nullsafe - An IllegalArgumentException will be thrown if the array passed
     * as a parameter is null.
     * @param list An array of Objects.
     * @return One of the Objects, randomly chosen.
     * @throws IllegalArgumentException If the array passed is NULL or if min is
     * greater than max.
     */
    public static Object getRandom(Object[] list) {
        
        Random rand = new Random(System.currentTimeMillis());
        return getRandom(list, rand);
        
    }
    
    /**
     * Returns a randomly selected Object from the supplied array.
     * <P>
     * Nullsafe - An IllegalArgumentException will be thrown if the array or
     * Random object passed as parameters are null.
     * @param list An array of Objects.
     * @param rand The seeded Random to use to select the random Object.
     * @return One of the Objects, randomly chosen.
     * @throws IllegalArgumentException If the array or Random passed is NULL.
     */
    public static Object getRandom(Object[] list, Random rand) {
        
        Object myObject;
        
        if (list == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "list");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "list");
            
        } else if (rand == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "rand");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "rand");
            
        } else {
            
            myObject = list[rand.nextInt(list.length)];
            
        }
        
        return myObject;
        
    }
    
    /**
     * Return a random int from between the supplied min and max.
     * <P>
     * The Random object will be seeded based on the current system time.
     * @param min The minimum value the number can have.
     * @param max The maximum value the number can have.
     * @return The random number.
     */
    public static int getRandom(int min, int max) {
        
        Random rand = new Random(System.currentTimeMillis());
        return getRandom(min, max, rand);
        
    }
    
    /**
     * Return a random int from between the supplied min and max.
     * <P>
     * Nullsafe - An IllegalArgumentException will be thrown if the Random
     * passed as a parameter is null.
     * @param min The minimum value the number can have.
     * @param max The maximum value the number can have.
     * @param rand The seeded Random to use to select the random int.
     * @return The number chosen randomly.
     * @throws IllegalArgumentException If the Random object passed is NULL or
     * if the min is greater than the max.
     */
    public static int getRandom(int min, int max, Random rand) {
        
        int randomInt;
        
        if (rand == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "rand");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "rand");
            
        } else if (min > max){
            
            LOGGER.log(Level.WARNING, "The min provided was greater than the "
                    + "max");
            throw new IllegalArgumentException("The min provided was greater " +
                    "than the max");
            
        } else {
            
            int interval = max - min;
            randomInt = (int) (min + (interval * rand.nextDouble()));
        }
        
        return randomInt;
        
    }
    
    /**
     * Return a random int from the supplied int array.
     * <P>
     * The Random object will be seeded based on the current system time.
     * <P>
     * Nullsafe - An IllegalArgumentException will be thrown if the array
     * passed as a parameter is null.
     * @param aList The array of int's.
     * @return The number chosen randomly.
     * @throws IllegalArgumentException If the array passed is NULL.
     */
    public static int getRandom(int[] aList) {
        
        Random rand = new Random(System.currentTimeMillis());
        return getRandom(aList, rand);
        
    }
    
    /**
     * Return a random int from the supplied int array.
     * <P>
     * Nullsafe - An IllegalArgumentException will be thrown if the Random
     * passed as a parameter is null.
     * @param aList The array of int's.
     * @param rand The seeded Random to use to select the random int.
     * @return The number chosen randomly.
     * @throws IllegalArgumentException If the Random passed is NULL.
     */
    public static int getRandom(int[] aList, Random rand) {
        
        int myInt;
        
        if (rand == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "rand");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "rand");
            
        } else {
            
            myInt = aList[rand.nextInt(aList.length)];
            
        }
        
        return myInt;
        
    }
    
    /**
     * Return a random long from between the supplied min and max.
     * <P>
     * The Random object will be seeded based on the current system time.
     * @param min The minimum value the number can have.
     * @param max The maximum value the number can have.
     * @return The number chosen randomly.
     * @throws A IllegalArgumentException if the min is greater than the max.
     */
    public static long getRandom(long min, long max) {
        
        Random rand = new Random(System.currentTimeMillis());
        return getRandom(min, max, rand);
        
    }
    
    /**
     * Return a random long from between the supplied min and max.
     * <P>
     * Nullsafe - An IllegalArgumentException will be thrown if the Random
     * passed as a parameter is null.
     * @param min The minimum value the number can have.
     * @param max The maximum value the number can have.
     * @param rand The seeded Random object to use to generate the random value.
     * @return The number chosen randomly.
     * @throws IllegalArgumentException If the Random passed is NULL or if the
     * min is greater than the max.
     */
    public static long getRandom(long min, long max, Random rand) {
        
        long randomLong;
        
        if (rand == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "rand");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "rand");
            
        } else if (min > max) {
            
            LOGGER.log(Level.WARNING, "The min provided was greater than the "
                    + "max");
            throw new IllegalArgumentException("The min provided was greater " +
                    "than the max");
            
        } else {
            
            long interval = max - min;
            randomLong = (long) (min + (interval * rand.nextDouble()));
        }
        
        return randomLong;
        
    }
    
    /**
     * Return a random double from between the supplied min and max.
     * <P>
     * The Random object will be seeded based on the current system time.
     * @param min The minimum value the number can have.
     * @param max The maximum value the number can have.
     * @return The number chosen randomly.
     * @throws IllegalArgumentException if the min is greater than the max.
     */
    public static double getRandom(double min, double max) {
        
        Random rand = new Random(System.currentTimeMillis());
        return getRandom(min, max, rand);
    }
    
    /**
     * Return a random double from between the supplied min and max.
     * <P>
     * Nullsafe - An IllegalArgumentException will be thrown if the array passed
     * as a parameter is null.
     * @param min The minimum value the number can have.
     * @param max The maximum value the number can have.
     * @param rand The seeded Random object to use to generate the random value.
     * @return The number chosen randomly.
     * @throws IllegalArgumentException If the Random passed is NULL or if the
     * min is greater than the max.
     */
    public static double getRandom(double min, double max, Random rand) {
        
        double randomDouble;
        
        if (rand == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "rand");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "rand");
            
        } else if (min > max) {
            
            LOGGER.log(Level.WARNING, "The min provided was greater than the "
                    + "max");
            throw new IllegalArgumentException("The min provided was greater " +
                    "than the max");
            
        } else {
            
            double interval = max - min;
            randomDouble = min + (interval * rand.nextDouble());
            
        }
        return randomDouble;
        
    }
    
    /**
     * Return a random Boolean.TRUE or Boolean.FALSE object based on the current
     * system time.
     * @return The Boolean value.
     */
    public static Boolean getRandomBoolean() {
        
        return Boolean.valueOf(System.currentTimeMillis() % 2 == 0);
        
    }
    
    /**
     * Return a random char from the provided String.
     * <P>
     * The Random object used will be seeded based on the current system time.
     * <P>
     * Nullsafe - An IllegalArgumentException will be thrown if if either the
     * String or Random passed as a parameters is null.
     * @param theString The String to select the char from.
     * @return The char selected.
     * @throws IllegalArgumentException If the String passed is NULL.
     */
    public static char getRandomCharFromString(String theString) {
        
        Random rand = new Random(System.currentTimeMillis());
        return getRandomCharFromString(theString, rand);
        
    }
    
    /**
     * Return a random char from the provided String.
     * <P>
     * The Random object used will be seeded based on the current system time.
     * <P>
     * Nullsafe - An IllegalArgumentException will be thrown if either the
     * String or Random passed as a parameter is null.
     * @param theString The String to select the char from.
     * @param rand The seeded Random object to use to select the random char.
     * @return The char selected.
     * @throws IllegalArgumentException If the String or Random passed is NULL.
     */
    public static char getRandomCharFromString(String theString, Random rand) {
        
        char randomChar;
        
        if (theString == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "theString");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "theString");
            
        } else if (rand == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "rand");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "rand");
            
        } else {
            
            randomChar = theString.charAt(rand.nextInt(theString.length()));
            
        }
        
        return randomChar;
        
    }
    
    /**
     * Return a random String from the provided array.
     * <P>
     * Nullsafe - An IllegalArgumentException will be thrown if the array passed
     * as a parameter is null.
     * @param theStrings The array of String objects to choose from.
     * @return The randomly selected String.
     * @throws IllegalArgumentException If the array passed is NULL.
     */
    public static String getRandomStringFromArray(String[] theStrings) {
        
        Random rand = new Random(System.currentTimeMillis());
        return getRandomStringFromArray(theStrings, rand);
        
    }
    
    /**
     * Return a random String from the provided array.
     * <P>
     * The Random object used will be seeded based on the current system time.
     * <P>
     * Nullsafe - An IllegalArgumentException will be thrown if either the array
     * or Random object passed as parameters is null.
     * @param theStrings The array of String objects to choose from.
     * @param rand The seeded Random object to use to select the random String.
     * @return The randomly selected String.
     * @throws IllegalArgumentException If the array or Random passed is NULL.
     */
    public static String getRandomStringFromArray(String[] theStrings,
            Random rand) {
        
        String myString;
        
        if (theStrings == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "theStrings");
            throw new IllegalArgumentException(NULL_PARAMETER_EX
                    + "theStrings");
            
        } else if (rand == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "rand");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "rand");
            
        } else {
            
            myString = theStrings[rand.nextInt(theStrings.length)];
            
        }
        
        return myString;
        
    }
    
    /**
     * Generates an random alpha-numeric String, based on a provided pattern.
     * <P>
     * Nullsafe - An IllegalArgumentException will be thrown if the String
     * passed as a parameter is null.
     * <P>
     * NOTE: The Random object used to get the random characters is seeded
     * based on the current system time.  Therefore if you run this method with
     * the same input parameters on two seperate occasions, you will get two
     * different results.
     * <P>
     * NOTE: If the pattern provided is an empty String, an empoty String will
     * be returned.
     * @param aPattern A pattern like AAAANNNNNAAAA returns a string where A's
     * are replaced with random alphanumeric characters and 'NNN' is a spread
     * out version of an incrementing number - guaranteeing uniqueness within
     * the range of that number (remember must have at least one N).
     * <P>
     * NOTE: The only characters which matter are the 'N's.  These are replaced
     * with numerals as described.  All other characters are replaced with
     * random alpha characters.
     * @return String the generated number String.
     * @throws IllegalArgumentException If the String passed is NULL.
     */
    public static String getStringFromPattern(String aPattern) {
        
        String myString = null;
        
        if (aPattern == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "aPattern");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "aPattern");
            
        } else if(aPattern.length() == 0) {
            
            LOGGER.log(Level.WARNING, "The pattern String was empty");
            myString = "";
            
        } else {
            
            int myCountNumeric = 0;
            int myOverallLength = aPattern.length();
            
            for (int i = 0; i < myOverallLength; i++) {
                char c = aPattern.charAt(i);
                if (c == 'N') {
                    myCountNumeric++;
                }
            }
            
            // Get a new integer of length myCountNumeric
            String myNumStr = getNumericString(myCountNumeric);
            StringBuffer myRetval = new StringBuffer();
            int myNumbersUsed = 0;
            for (int i = 0; i < myOverallLength; i++) {
                char c = aPattern.charAt(i);
                if (c == 'N') {
                    myRetval.append(myNumStr.charAt(myNumbersUsed));
                    myNumbersUsed++;
                } else {
                    myRetval.append((char) getRandom(ASCII_A, ASCII_Z));
                }
                
                myString = myRetval.toString();
            }
            
        }
        
        return myString;
        
    }
    
    /**
     * This class selects an option for an array of choices provided based on a
     * percentage chance of selecting each.
     * <P>
     * If the weighting does not total 100 or the arrays are of different sizes
     * NULL is returned.
     * <P>
     * Nullsafe - An IllegalArgumentException will be returned if either array
     * passed as a parameter is null.
     * @param weighting An array containing the relative weightings for each
     * option.
     * @param options An array containing the options to select from.
     * @return The selected option.
     * @throws IllegalArgumentException If the array passed is NULL, the arrays
     * are of different lengths or the weightings don't add up to 100.
     */
    public static Object selectRandomPercentWeighted(int[] weighting,
            Object[] options) {
        
        Object myObject;
        
        if (weighting == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "weighting");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "weighting");
            
        } else if (options == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "options");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "options");
            
        } else if (weighting.length != options.length) {
            
            LOGGER.log(Level.WARNING, "The arrays provided are of different "
                    + "lengths");
            throw new IllegalArgumentException("The arrays provided are of "
                    + "different lengths");
            
        } else {
            
            int total = 0;
            for (int i = 0; i < weighting.length; i++) {
                total += weighting[i];
            }
            
            if (total != 100) {
                
                LOGGER.log(Level.WARNING, "The weightings don't add up to 100");
                throw new IllegalArgumentException("The weightings don't add up to "
                        + "100");
                
            }
            
            // Get a random number between 1 and 100
            int randomSelection = getRandom(1, 100);
            
            // Find out which option this falls within
            int runningTotal = 0;
            int counter;
            for (counter = 0; counter < weighting.length; counter++) {
                runningTotal += weighting[counter];
                if (randomSelection <= runningTotal) {
                    break;
                }
            }
            
            myObject = options[counter];
            
        }
        
        return myObject;
        
    }
    
    /**
     * Utility method to return an incrementing numeric String (incremented
     * since the last call to this method).
     * @param length Length of the String.
     * @return The value as a String.
     */
    private static String getNumericString(int length) {
        
        Integer myLastValue = myCounters.get(Integer.valueOf(length));
        
        if (myLastValue == null) {
            myLastValue = Integer.valueOf(1);
        } else {
            int newValue = myLastValue.intValue() + 1;
            if (newValue == (length * BASE_10)) {
                newValue = 1;
            }
            myLastValue = Integer.valueOf(newValue);
        }
        
        myCounters.put(Integer.valueOf(length), myLastValue);
        String myUnpaddedValue = myLastValue.toString();
        
        return StringUtils.paddingString(myUnpaddedValue, length, '0', true);
        
    }

    /**
     * Generates a random postcode as a <String> and returns it.  The Random 
     * used is seeded with the current system time in milliseconds.
     * <P>
     * Valid formats:
     * ^([A-PR-UWYZ0-9][A-HK-Y0-9][AEHMNPRTVXY0-9]?[ABEHMNPRVWXY0-9]?)
     * ({1,2}[0-9][ABD-HJLN-UW-Z]{2}|GIR 0AA)$
     * <P>
     * Note: this won't generate all valid formats, but a significant subset.
     * @return The generated postcode.
     */
    public static String nextRandomPostcode() {

        Random rand = new Random(System.currentTimeMillis());
        return nextRandomPostcode(rand);
        
    }
    
    /**
     * Generates a random postcode as a <String> and returns it.
     * Valid formats:
     * ^([A-PR-UWYZ0-9][A-HK-Y0-9][AEHMNPRTVXY0-9]?[ABEHMNPRVWXY0-9]?)
     * ({1,2}[0-9][ABD-HJLN-UW-Z]{2}|GIR 0AA)$
     * <P>
     * Note: this won't generate all valid formats, but a significant subset.
     * <P>
     * Nullsafe - An IllegalArgumentException will be thrown if the Random
     * passed as a parameter is null
     * @param rand The seeded Random object to use to generate the postcode.
     * @return The generated postcode.
     */
    public static String nextRandomPostcode(Random rand) {

        String postcode;
        
        if (rand == null) {   
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "rand");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "rand");
            
        } else {
            
            StringBuffer postcodeBuffer = new StringBuffer();
            postcodeBuffer.append(RandomGenerators.getRandomCharFromString("ABCDEFGHIJKLMNOPRSTUWYZ", rand));
            postcodeBuffer.append(RandomGenerators.getRandomCharFromString("ABCDEFGHKLMNOPRSTUWY", rand));
            postcodeBuffer.append(RandomGenerators.getRandomCharFromString("0123456789", rand));
            postcodeBuffer.append(RandomGenerators.getRandomCharFromString("ABEHMNPRVWXY", rand));
            postcodeBuffer.append(' ');
            postcodeBuffer.append(RandomGenerators.getRandomCharFromString("0123456789", rand));
            postcodeBuffer.append(RandomGenerators.getRandomCharFromString("ABDEFGHJLNOPQRSTUWXYZ", rand));
            postcodeBuffer.append(RandomGenerators.getRandomCharFromString("ABDEFGHJLNOPQRSTUWXYZ", rand));
            postcode = postcodeBuffer.toString();
            
        }

        return postcode;
        
    }
    
}